// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © EMA34TRADER
//@version=6
indicator("Time Range ORB Detector [34ematrader]", shorttitle="TBR / ORB Detector", overlay=true,
     max_boxes_count=500, max_lines_count=500, max_labels_count=500)

// ═══════════════════════════════════════════════════════════════════════════
// Configurable time-range / ORB indicator
//   • Pick market (sets default point target)
//   • Pick session window (TBR 8:12–9:12, 15m ORB, 30m ORB, IB, London, custom)
//   • Modes: fade first break  |  breakout  |  breakout then reversal
// Edited by 34ematrader
// ═══════════════════════════════════════════════════════════════════════════

// ── Market ────────────────────────────────────────────────────────────────
grpMkt = "Market"
iMkt = input.string("NQ", "Market",
     options=["NQ", "MNQ", "ES", "MES", "YM", "MYM", "RTY", "M2K", "GC", "CL", "Custom"],
     group=grpMkt,
     tooltip="Sets the default fixed-point target. Override below if you want a different size.")
iCustomPts = input.float(50.0, "Custom / override point target", minval=0.25, step=0.25, group=grpMkt)
iUseMktPts = input.bool(true, "Use market default point target", group=grpMkt)

f_mkt_pts() =>
    switch iMkt
        "NQ"  => 50.0
        "MNQ" => 50.0
        "ES"  => 12.0
        "MES" => 12.0
        "YM"  => 80.0
        "MYM" => 80.0
        "RTY" => 15.0
        "M2K" => 15.0
        "GC"  => 8.0
        "CL"  => 0.50
        => iCustomPts

tgtPts = iUseMktPts and iMkt != "Custom" ? f_mkt_pts() : iCustomPts

// ── Session / time window ─────────────────────────────────────────────────
grpTime = "Session / time window"
iPreset = input.string("NY TBR 8:12–9:12", "Session preset",
     options=["NY TBR 8:12–9:12",
              "15-min ORB 9:30–9:45",
              "5-min ORB 9:30–9:35",
              "30-min ORB 9:30–10:00",
              "Initial Balance 9:30–10:30",
              "London TBR 1:12–2:12",
              "Custom start + end",
              "Custom start + duration"],
     group=grpTime)
iStartHIn = input.int(9,  "Custom start hour",   minval=0, maxval=23, group=grpTime)
iStartMIn = input.int(30, "Custom start minute", minval=0, maxval=59, group=grpTime)
iEndHIn   = input.int(9,  "Custom end hour",     minval=0, maxval=23, group=grpTime)
iEndMIn   = input.int(45, "Custom end minute",   minval=0, maxval=59, group=grpTime)
iDurMin   = input.int(15, "Custom duration (minutes)", minval=1, maxval=360, group=grpTime)
iTz       = input.string("America/New_York", "Timezone",
     options=["America/New_York", "America/Chicago", "America/Los_Angeles", "UTC", "Europe/London"],
     group=grpTime)
iShowDays = input.int(10, "Historical sessions to keep", minval=1, maxval=40, group=grpTime)
iSignalUntilH = input.int(16, "Stop new signals after hour", minval=0, maxval=23, group=grpTime)
iSignalUntilM = input.int(0,  "Stop new signals after minute", minval=0, maxval=59, group=grpTime)

// Resolve window from preset. Duration presets compute end from start + minutes.
f_win() =>
    int sh = 8
    int sm = 12
    int eh = 9
    int em = 12
    switch iPreset
        "NY TBR 8:12–9:12" =>
            sh := 8
            sm := 12
            eh := 9
            em := 12
        "15-min ORB 9:30–9:45" =>
            sh := 9
            sm := 30
            eh := 9
            em := 45
        "5-min ORB 9:30–9:35" =>
            sh := 9
            sm := 30
            eh := 9
            em := 35
        "30-min ORB 9:30–10:00" =>
            sh := 9
            sm := 30
            eh := 10
            em := 0
        "Initial Balance 9:30–10:30" =>
            sh := 9
            sm := 30
            eh := 10
            em := 30
        "London TBR 1:12–2:12" =>
            sh := 1
            sm := 12
            eh := 2
            em := 12
        "Custom start + duration" =>
            sh := iStartHIn
            sm := iStartMIn
            int total = iStartHIn * 60 + iStartMIn + iDurMin
            eh := int(math.floor(total / 60.0)) % 24
            em := total % 60
        "Custom start + end" =>
            sh := iStartHIn
            sm := iStartMIn
            eh := iEndHIn
            em := iEndMIn
        =>
            sh := iStartHIn
            sm := iStartMIn
            eh := iEndHIn
            em := iEndMIn
    [sh, sm, eh, em]

[iStartH, iStartM, iEndH, iEndM] = f_win()

// ── Signal logic ──────────────────────────────────────────────────────────
grpSig = "Break & signal logic"
iStyle = input.string("Breakout then reversal", "Trade style",
     options=["Reversal (fade first break)", "Breakout (trade first break)", "Breakout then reversal"],
     group=grpSig,
     tooltip="Reversal: first side taken → fade toward the opposite end (original TBR post).\nBreakout: first side taken → trade WITH the break.\nBreakout then reversal: first signal WITH the break targeting the opposite range end / points; if the other side is later taken, fire the opposite reversal signal.")
iBreakMode = input.string("Wick", "Break detection", options=["Wick", "Close"], group=grpSig)
iConfirm = input.string("Sweep only (first break)", "Entry confirmation",
     options=["Sweep only (first break)", "Sweep + reclaim close", "Sweep + opposing candle"],
     group=grpSig)
iAllowRev = input.bool(true, "Allow second (reversal) signal after first break", group=grpSig,
     tooltip="Used by 'Breakout then reversal'. After the first side is taken and the first signal prints, a break of the opposite side prints the reversal.")
iRequireAfterRange = input.bool(true, "Only detect breaks AFTER the window ends", group=grpSig)

// ── Targets ───────────────────────────────────────────────────────────────
grpTgt = "Targets"
iShowOpp   = input.bool(true, "Target: opposing range end (high or low)", group=grpTgt)
iShowEq    = input.bool(true, "Target: 50% equilibrium", group=grpTgt)
iShowPts   = input.bool(true, "Target: fixed points", group=grpTgt)
iShowMM    = input.bool(false, "Target: measured move (1x range height)", group=grpTgt)
iExtendHrs = input.int(6, "Extend levels (hours after window end)", minval=1, maxval=20, group=grpTgt)

grpRisk = "Take profit / Stop loss"
iAtrLen    = input.int(14, "ATR length", minval=1, maxval=100, group=grpRisk)
iTpAtr     = input.float(1.0, "Take profit (ATR multiples)", minval=0.1, step=0.1, group=grpRisk)
iSlAtr     = input.float(0.5, "Stop loss (ATR multiples)", minval=0.1, step=0.1, group=grpRisk)
iSlMode    = input.string("ATR", "Stop loss type",
     options=["ATR", "Swing / pivot", "Tighter of ATR or swing", "Wider of ATR or swing"],
     group=grpRisk,
     tooltip="ATR: SL = entry ± SL ATR multiple.\nSwing / pivot: SL beyond the last confirmed pivot (or lookback swing if no pivot yet).\nTighter / wider: combine both.")
iPivotL    = input.int(5, "Pivot left bars", minval=1, maxval=50, group=grpRisk)
iPivotR    = input.int(5, "Pivot right bars", minval=1, maxval=50, group=grpRisk)
iSwingLb   = input.int(10, "Swing lookback (fallback)", minval=2, maxval=100, group=grpRisk,
     tooltip="Used when a confirmed pivot is not available yet, or as the swing reference.")
iSwingBufAtr = input.float(0.1, "Swing SL buffer (ATR)", minval=0.0, step=0.05, group=grpRisk,
     tooltip="Extra room beyond the swing/pivot so the stop is not glued to the wick.")
iShowTpsl  = input.bool(true, "Draw TP / SL lines on the trade", group=grpRisk)
iExpireEod = input.bool(true, "Expire open paper trade at next NY day", group=grpRisk)
iStatsOnRev = input.bool(true, "Include reversal signals in win rate", group=grpRisk)

// ── Visuals ───────────────────────────────────────────────────────────────
grpVis = "Visuals"
iShowBox     = input.bool(true, "Show range box", group=grpVis)
iShowExt     = input.bool(true, "Extend High / Low / Mid after window", group=grpVis)
iShowMid     = input.bool(true, "Show mid line", group=grpVis)
iShowLabels  = input.bool(true, "HIGH / LOW labels", group=grpVis)
iShowSweeps  = input.bool(true, "Mark first sweep", group=grpVis)
iShowSignals = input.bool(true, "Buy / Sell shapes", group=grpVis)
iShowTgtBox  = input.bool(true, "Paint target zone after signal", group=grpVis)
iShowTable   = input.bool(true, "Session status table", group=grpVis)
iTablePos    = input.string("Top Right", "Table position",
     options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group=grpVis)

colBoxBg   = input.color(color.new(#6b1212, 78), "Box fill", group=grpVis)
colBoxBd   = input.color(color.new(#c43c3c, 20), "Box border / range lines", group=grpVis)
colMid     = input.color(color.new(#ffffff, 55), "Mid line", group=grpVis)
colHighLbl = input.color(#ff6b6b, "Range High label", group=grpVis)
colLowLbl  = input.color(#ff6b6b, "Range Low label", group=grpVis)
colBuy     = input.color(#00e676, "Buy color", group=grpVis)
colSell    = input.color(#ff1744, "Sell color", group=grpVis)
colSweep   = input.color(#ffd54f, "Sweep marker", group=grpVis)
colRev     = input.color(#ff9100, "Reversal signal color", group=grpVis)
colTgtLong = input.color(color.new(#1b5e20, 72), "Long target zone", group=grpVis)
colTgtShrt = input.color(color.new(#7f1d1d, 72), "Short target zone", group=grpVis)

grpAlert = "Alerts"
iAlertSweep = input.bool(true, "Alert on first sweep", group=grpAlert)
iAlertSig   = input.bool(true, "Alert on buy / sell", group=grpAlert)
iAlertRev   = input.bool(true, "Alert on reversal signal", group=grpAlert)
iAlertTgt   = input.bool(true, "Alert when opposing end is tagged", group=grpAlert)

// ── Time helpers ──────────────────────────────────────────────────────────
nyY   = year(time, iTz)
nyMo  = month(time, iTz)
nyD   = dayofmonth(time, iTz)
nyDow = dayofweek(time, iTz)

f_ts(y, m, d, h, min) =>
    timestamp(iTz, y, m, d, h, min)

rangeStart = f_ts(nyY, nyMo, nyD, iStartH, iStartM)
rangeEnd   = f_ts(nyY, nyMo, nyD, iEndH, iEndM)
// Handle window that crosses midnight (rare, but custom duration can)
rangeEnd := rangeEnd <= rangeStart ? rangeEnd + 24 * 60 * 60 * 1000 : rangeEnd
signalCut  = f_ts(nyY, nyMo, nyD, iSignalUntilH, iSignalUntilM)
signalCut := signalCut <= rangeEnd ? signalCut + 24 * 60 * 60 * 1000 : signalCut

inBuild     = time >= rangeStart and time < rangeEnd
afterRng    = time >= rangeEnd
inSignalWin = time >= rangeEnd and time < signalCut
newNyDay    = ta.change(nyD) != 0 or ta.change(nyY) != 0
isWeekday   = nyDow != dayofweek.saturday and nyDow != dayofweek.sunday

f_pad(n) =>
    n < 10 ? "0" + str.tostring(n) : str.tostring(n)

winLabel = f_pad(iStartH) + ":" + f_pad(iStartM) + "–" + f_pad(iEndH) + ":" + f_pad(iEndM)

// ── Session state ─────────────────────────────────────────────────────────
var float rngHi = na
var float rngLo = na
var int   rngStartBar = na
var int   rngEndBar = na
var bool  locked = false
var int   firstSide = 0          // 1 = high first, -1 = low first
var int   secondSide = 0         // opposite side taken later
var int   sweepBar = na
var float sweepPx = na
var int   secondBar = na
var bool  firstSigDone = false
var bool  revSigDone = false
var int   firstDir = 0           // 1 long, -1 short
var int   revDir = 0
var float firstPx = na
var float revPx = na
var bool  oppHit = false
var bool  ptsHit = false
var bool  eqHit = false
var bool  mmHit = false

var box   rngBox = na
var line  lnHi = na
var line  lnLo = na
var line  lnMid = na
var box   tgtBox = na
var line  lnOpp = na
var line  lnPts = na
var line  lnMm = na
var line  lnTp = na
var line  lnSl = na
var label lbTp = na
var label lbSl = na

// Paper-trade / win-rate state (does NOT reset each session)
var int   posDir = 0
var float posEntry = na
var float posTp = na
var float posSl = na
var int   posBar = na
var bool  posIsRev = false
var int   statWins = 0
var int   statLoss = 0
var int   statExp = 0
var float statWinR = 0.0
var float statLossR = 0.0

atrVal = ta.atr(iAtrLen)
phConf = ta.pivothigh(high, iPivotL, iPivotR)
plConf = ta.pivotlow(low, iPivotL, iPivotR)
var float lastPh = na
var float lastPl = na
if not na(phConf)
    lastPh := phConf
if not na(plConf)
    lastPl := plConf
swingHiFb = ta.highest(high, iSwingLb)
swingLoFb = ta.lowest(low, iSwingLb)

var box[]   histBoxes = array.new_box()
var line[]  histLines = array.new_line()
var label[] histLbls  = array.new_label()

f_cap() =>
    while array.size(histBoxes) > iShowDays
        box.delete(array.shift(histBoxes))
    while array.size(histLines) > iShowDays * 8
        line.delete(array.shift(histLines))
    while array.size(histLbls) > iShowDays * 8
        label.delete(array.shift(histLbls))

f_push_box(b) =>
    if not na(b)
        array.push(histBoxes, b)
        f_cap()

f_push_line(l) =>
    if not na(l)
        array.push(histLines, l)

f_push_lbl(l) =>
    if not na(l)
        array.push(histLbls, l)

if newNyDay and posDir != 0 and iExpireEod
    statExp += 1
    posDir := 0
    posEntry := na
    posTp := na
    posSl := na
    posBar := na
    posIsRev := false
    lnTp := na
    lnSl := na
    if not na(lbTp)
        label.delete(lbTp)
        lbTp := na
    if not na(lbSl)
        label.delete(lbSl)
        lbSl := na

if newNyDay and isWeekday
    rngHi := na
    rngLo := na
    rngStartBar := na
    rngEndBar := na
    locked := false
    firstSide := 0
    secondSide := 0
    sweepBar := na
    sweepPx := na
    secondBar := na
    firstSigDone := false
    revSigDone := false
    firstDir := 0
    revDir := 0
    firstPx := na
    revPx := na
    oppHit := false
    ptsHit := false
    eqHit := false
    mmHit := false
    rngBox := na
    lnHi := na
    lnLo := na
    lnMid := na
    tgtBox := na
    lnOpp := na
    lnPts := na
    lnMm := na

// ── Build / lock range ────────────────────────────────────────────────────
if isWeekday and inBuild
    if na(rngHi)
        rngHi := high
        rngLo := low
        rngStartBar := bar_index
    else
        rngHi := math.max(rngHi, high)
        rngLo := math.min(rngLo, low)
    locked := false

justLocked = isWeekday and locked[1] == false and afterRng and not na(rngHi)
if justLocked
    locked := true
    rngEndBar := bar_index

if isWeekday and afterRng and not na(rngHi)
    locked := true
    if na(rngEndBar)
        rngEndBar := bar_index

rngMid = not na(rngHi) and not na(rngLo) ? math.avg(rngHi, rngLo) : na
rngHgt = not na(rngHi) and not na(rngLo) ? rngHi - rngLo : na

// ── Draw box + levels ─────────────────────────────────────────────────────
if iShowBox and isWeekday and not na(rngHi) and not na(rngStartBar)
    if na(rngBox)
        rngBox := box.new(rngStartBar, rngHi, bar_index, rngLo,
             bgcolor=colBoxBg, border_color=colBoxBd, border_width=1, border_style=line.style_dotted)
        f_push_box(rngBox)
    else
        box.set_top(rngBox, rngHi)
        box.set_bottom(rngBox, rngLo)
        box.set_right(rngBox, bar_index)

if iShowExt and locked and not na(rngHi)
    extRight = bar_index
    if na(lnHi)
        lnHi := line.new(rngEndBar, rngHi, extRight, rngHi, color=colBoxBd, width=1, style=line.style_solid)
        lnLo := line.new(rngEndBar, rngLo, extRight, rngLo, color=colBoxBd, width=1, style=line.style_solid)
        f_push_line(lnHi)
        f_push_line(lnLo)
        if iShowMid
            lnMid := line.new(rngStartBar, rngMid, extRight, rngMid, color=colMid, width=1, style=line.style_dashed)
            f_push_line(lnMid)
    else
        line.set_xy2(lnHi, extRight, rngHi)
        line.set_xy2(lnLo, extRight, rngLo)
        if not na(lnMid)
            line.set_xy2(lnMid, extRight, rngMid)
            line.set_y1(lnMid, rngMid)
            line.set_y2(lnMid, rngMid)

if iShowLabels and justLocked
    hiName = str.contains(iPreset, "ORB") or str.contains(iPreset, "Balance") ? "OR HIGH" : "TBR HIGH"
    loName = str.contains(iPreset, "ORB") or str.contains(iPreset, "Balance") ? "OR LOW"  : "TBR LOW"
    f_push_lbl(label.new(rngStartBar, rngHi, hiName, style=label.style_label_down,
         color=color.new(colHighLbl, 100), textcolor=colHighLbl, size=size.small))
    f_push_lbl(label.new(rngStartBar, rngLo, loName, style=label.style_label_up,
         color=color.new(colLowLbl, 100), textcolor=colLowLbl, size=size.small))
    if iShowEq
        f_push_lbl(label.new(rngEndBar, rngMid, "EQ 50%", style=label.style_none, textcolor=colMid, size=size.tiny))

// ── Break detection ───────────────────────────────────────────────────────
brokeHiWick  = locked and not na(rngHi) and high > rngHi
brokeLoWick  = locked and not na(rngLo) and low  < rngLo
brokeHiClose = locked and not na(rngHi) and close > rngHi
brokeLoClose = locked and not na(rngLo) and close < rngLo
brokeHi = iBreakMode == "Close" ? brokeHiClose : brokeHiWick
brokeLo = iBreakMode == "Close" ? brokeLoClose : brokeLoWick

canDetect = locked and firstSide == 0 and not na(rngHi)
canDetect := iRequireAfterRange ? canDetect and inSignalWin : canDetect and (inBuild or inSignalWin)

if canDetect
    if brokeHi and not brokeLo
        firstSide := 1
        sweepBar := bar_index
        sweepPx := high
    else if brokeLo and not brokeHi
        firstSide := -1
        sweepBar := bar_index
        sweepPx := low

sweepNow = firstSide != 0 and firstSide[1] == 0

// Opposite-side break after the first sweep (the "then reversal" leg)
canSecond = locked and firstSide != 0 and secondSide == 0 and inSignalWin and bar_index > sweepBar
if canSecond
    if firstSide == 1 and brokeLo
        secondSide := -1
        secondBar := bar_index
    else if firstSide == -1 and brokeHi
        secondSide := 1
        secondBar := bar_index

secondNow = secondSide != 0 and secondSide[1] == 0

if iShowSweeps and sweepNow
    if firstSide == 1
        f_push_lbl(label.new(bar_index, high, "HIGH TAKEN", style=label.style_label_down,
             color=color.new(colSweep, 10), textcolor=color.black, size=size.small))
    else
        f_push_lbl(label.new(bar_index, low, "LOW TAKEN", style=label.style_label_up,
             color=color.new(colSweep, 10), textcolor=color.black, size=size.small))

if iShowSweeps and secondNow
    f_push_lbl(label.new(bar_index, secondSide == 1 ? high : low, "OPPOSITE TAKEN",
         style=secondSide == 1 ? label.style_label_down : label.style_label_up,
         color=color.new(colRev, 10), textcolor=color.black, size=size.small))

// ── Confirmation helpers ──────────────────────────────────────────────────
insideAfterSweep = firstSide != 0 and bar_index > sweepBar and close <= rngHi and close >= rngLo
oppBear = firstSide == 1 and bar_index > sweepBar and close < open
oppBull = firstSide == -1 and bar_index > sweepBar and close > open

firstLongReady  = false
firstShortReady = false
revLongReady    = false
revShortReady   = false

// First signal direction by style
// Reversal fade: high taken → SELL, low taken → BUY
// Breakout:      high taken → BUY,  low taken → SELL
breakoutStyle = iStyle == "Breakout (trade first break)" or iStyle == "Breakout then reversal"
fadeStyle     = iStyle == "Reversal (fade first break)"

if firstSide != 0 and inSignalWin and not firstSigDone
    rawLong  = fadeStyle ? firstSide == -1 : firstSide == 1
    rawShort = fadeStyle ? firstSide == 1  : firstSide == -1
    switch iConfirm
        "Sweep only (first break)" =>
            firstLongReady  := rawLong and sweepNow
            firstShortReady := rawShort and sweepNow
        "Sweep + reclaim close" =>
            firstLongReady  := rawLong and insideAfterSweep
            firstShortReady := rawShort and insideAfterSweep
        "Sweep + opposing candle" =>
            firstLongReady  := rawLong and (fadeStyle ? oppBull : close > open and bar_index > sweepBar)
            firstShortReady := rawShort and (fadeStyle ? oppBear : close < open and bar_index > sweepBar)

if firstLongReady
    firstSigDone := true
    firstDir := 1
    firstPx := close

if firstShortReady
    firstSigDone := true
    firstDir := -1
    firstPx := close

// Reversal signal: opposite side taken after the first break
// Always fades the SECOND break (target the first side / original high or low)
wantRev = (iStyle == "Breakout then reversal" or iStyle == "Reversal (fade first break)") and iAllowRev
if wantRev and firstSigDone and secondNow and not revSigDone and inSignalWin
    if secondSide == 1
        revShortReady := false
        revLongReady := true
    else if secondSide == -1
        revLongReady := false
        revShortReady := true

if revLongReady
    revSigDone := true
    revDir := 1
    revPx := close

if revShortReady
    revSigDone := true
    revDir := -1
    revPx := close

buyFirst  = firstDir == 1 and firstDir[1] != 1 and firstSigDone and not firstSigDone[1]
sellFirst = firstDir == -1 and firstDir[1] != -1 and firstSigDone and not firstSigDone[1]
buyRev    = revDir == 1 and revDir[1] != 1 and revSigDone and not revSigDone[1]
sellRev   = revDir == -1 and revDir[1] != -1 and revSigDone and not revSigDone[1]

plotshape(iShowSignals and buyFirst,  title="BUY",      style=shape.labelup,   location=location.belowbar, color=colBuy,  textcolor=color.white, text="BUY",      size=size.small)
plotshape(iShowSignals and sellFirst, title="SELL",     style=shape.labeldown, location=location.abovebar, color=colSell, textcolor=color.white, text="SELL",     size=size.small)
plotshape(iShowSignals and buyRev,    title="REV BUY",  style=shape.labelup,   location=location.belowbar, color=colRev,  textcolor=color.white, text="REV BUY",  size=size.small)
plotshape(iShowSignals and sellRev,   title="REV SELL", style=shape.labeldown, location=location.abovebar, color=colRev,  textcolor=color.white, text="REV SELL", size=size.small)

// ── ATR / swing TP-SL paper trades (win rate) ─────────────────────────────
f_swing_sl(dir, entry) =>
    buf = nz(atrVal, 0.0) * iSwingBufAtr
    float slPx = na
    if dir == 1
        pv = not na(lastPl) and lastPl < entry ? lastPl : swingLoFb
        slPx := pv - buf
    else
        pv = not na(lastPh) and lastPh > entry ? lastPh : swingHiFb
        slPx := pv + buf
    slPx

f_final_sl(dir, entry) =>
    atrSl = entry - dir * iSlAtr * nz(atrVal, 0.0)
    swSl  = f_swing_sl(dir, entry)
    float slPx = atrSl
    if iSlMode == "ATR"
        slPx := atrSl
    else if iSlMode == "Swing / pivot"
        slPx := swSl
    else if iSlMode == "Tighter of ATR or swing"
        slPx := dir == 1 ? math.max(atrSl, swSl) : math.min(atrSl, swSl)
    else
        slPx := dir == 1 ? math.min(atrSl, swSl) : math.max(atrSl, swSl)
    // If swing put the stop on the wrong side of entry, fall back to ATR
    if dir == 1 and slPx >= entry
        slPx := atrSl
    if dir == -1 and slPx <= entry
        slPx := atrSl
    slPx

tpHitNow = false
slHitNow = false

if posDir != 0 and bar_index > posBar and not na(posTp) and not na(posSl)
    slHitNow := posDir == 1 ? low <= posSl : high >= posSl
    tpHitNow := posDir == 1 ? high >= posTp : low <= posTp
    if slHitNow and tpHitNow
        slHitNow := true
        tpHitNow := false
        statLoss += 1
        statLossR += 1.0
        if iShowTpsl and not na(lbSl)
            label.set_xy(lbSl, bar_index, posSl)
            label.set_text(lbSl, "SL HIT  " + str.tostring(posSl, format.mintick))
            label.set_style(lbSl, label.style_label_left)
            label.set_color(lbSl, color.new(colSell, 0))
            label.set_textcolor(lbSl, color.white)
            label.set_size(lbSl, size.small)
        if not na(lbTp)
            label.delete(lbTp)
            lbTp := na
        posDir := 0
    else if slHitNow
        statLoss += 1
        statLossR += 1.0
        if iShowTpsl and not na(lbSl)
            label.set_xy(lbSl, bar_index, posSl)
            label.set_text(lbSl, "SL HIT  " + str.tostring(posSl, format.mintick))
            label.set_style(lbSl, label.style_label_left)
            label.set_color(lbSl, color.new(colSell, 0))
            label.set_textcolor(lbSl, color.white)
            label.set_size(lbSl, size.small)
        if not na(lbTp)
            label.delete(lbTp)
            lbTp := na
        posDir := 0
    else if tpHitNow
        risk = math.abs(posEntry - posSl)
        reward = math.abs(posTp - posEntry)
        statWins += 1
        statWinR += risk > 0 ? reward / risk : iTpAtr / iSlAtr
        if iShowTpsl and not na(lbTp)
            label.set_xy(lbTp, bar_index, posTp)
            label.set_text(lbTp, "TP HIT  " + str.tostring(posTp, format.mintick))
            label.set_style(lbTp, label.style_label_left)
            label.set_color(lbTp, color.new(colBuy, 0))
            label.set_textcolor(lbTp, color.white)
            label.set_size(lbTp, size.small)
        if not na(lbSl)
            label.delete(lbSl)
            lbSl := na
        posDir := 0

openNow = buyFirst or sellFirst or (iStatsOnRev and (buyRev or sellRev))
openDir = (buyFirst or buyRev) ? 1 : (sellFirst or sellRev) ? -1 : 0
openIsRev = buyRev or sellRev

if openNow and openDir != 0
    if posDir != 0
        statExp += 1
        posDir := 0
    atrNow = nz(atrVal, 0.0)
    if atrNow > 0
        posDir := openDir
        posEntry := close
        posTp := close + openDir * iTpAtr * atrNow
        posSl := f_final_sl(openDir, close)
        posBar := bar_index
        posIsRev := openIsRev
        if iShowTpsl
            lnTp := line.new(bar_index, posTp, bar_index, posTp, color=color.new(colBuy, 0), width=2, style=line.style_solid)
            lnSl := line.new(bar_index, posSl, bar_index, posSl, color=color.new(colSell, 0), width=2, style=line.style_solid)
            f_push_line(lnTp)
            f_push_line(lnSl)
            slTag = iSlMode == "ATR" ? "SL  " + str.tostring(iSlAtr, "#.##") + "×ATR   " + str.tostring(posSl, format.mintick) : "SL  SWING   " + str.tostring(posSl, format.mintick)
            tpTag = "TP  " + str.tostring(iTpAtr, "#.##") + "×ATR   " + str.tostring(posTp, format.mintick)
            // Horizontal labels sit to the RIGHT of the latest candle; the pointer aims LEFT at that candle
            if not na(lbTp)
                label.delete(lbTp)
            if not na(lbSl)
                label.delete(lbSl)
            lbTp := label.new(bar_index, posTp, tpTag,
                 style=label.style_label_left, color=color.new(colBuy, 0), textcolor=color.white,
                 size=size.small, textalign=text.align_left, tooltip="Take profit")
            lbSl := label.new(bar_index, posSl, slTag,
                 style=label.style_label_left, color=color.new(colSell, 0), textcolor=color.white,
                 size=size.small, textalign=text.align_left, tooltip="Stop loss")

if posDir != 0 and iShowTpsl
    slTagLive = iSlMode == "ATR" ? "SL  " + str.tostring(iSlAtr, "#.##") + "×ATR   " + str.tostring(posSl, format.mintick) : "SL  SWING   " + str.tostring(posSl, format.mintick)
    tpTagLive = "TP  " + str.tostring(iTpAtr, "#.##") + "×ATR   " + str.tostring(posTp, format.mintick)
    if not na(lnTp)
        line.set_xy2(lnTp, bar_index, posTp)
        line.set_y1(lnTp, posTp)
        line.set_color(lnTp, color.new(colBuy, 0))
    if not na(lnSl)
        line.set_xy2(lnSl, bar_index, posSl)
        line.set_y1(lnSl, posSl)
        line.set_color(lnSl, color.new(colSell, 0))
    // Keep labels glued to the latest candle so the arrow always points at current price
    if not na(lbTp)
        label.set_xy(lbTp, bar_index, posTp)
        label.set_text(lbTp, tpTagLive)
        label.set_style(lbTp, label.style_label_left)
        label.set_color(lbTp, color.new(colBuy, 0))
        label.set_textcolor(lbTp, color.white)
        label.set_size(lbTp, size.small)
    if not na(lbSl)
        label.set_xy(lbSl, bar_index, posSl)
        label.set_text(lbSl, slTagLive)
        label.set_style(lbSl, label.style_label_left)
        label.set_color(lbSl, color.new(colSell, 0))
        label.set_textcolor(lbSl, color.white)
        label.set_size(lbSl, size.small)

statClosed = statWins + statLoss
winRate = statClosed > 0 ? 100.0 * statWins / statClosed : na

// ── Targets ───────────────────────────────────────────────────────────────
// First-signal targets:
//   Breakout long  → range high is already broken; target = high + pts / MM, also mark range low as invalidation
//   Fade short     → target = range low (opposing end)
activeDir = revSigDone ? revDir : firstDir
activePx  = revSigDone ? revPx  : firstPx
justSig   = buyFirst or sellFirst or buyRev or sellRev

longTgtOpp  = rngHi
shortTgtOpp = rngLo
longTgtPts  = activePx + tgtPts
shortTgtPts = activePx - tgtPts
longTgtMm   = rngHi + rngHgt
shortTgtMm  = rngLo - rngHgt

if justSig and iShowTgtBox and not na(rngHi)
    tgtTop = activeDir == 1 ? longTgtOpp : activePx
    tgtBot = activeDir == 1 ? activePx : shortTgtOpp
    // For breakout-with-the-move the opposing end may already be behind price;
    // still paint toward the remaining unused end when fading / reversing.
    if fadeStyle or buyRev or sellRev or not breakoutStyle
        tgtBox := box.new(bar_index, tgtTop, bar_index + 1, tgtBot,
             bgcolor=activeDir == 1 ? colTgtLong : colTgtShrt,
             border_color=activeDir == 1 ? colBuy : colSell,
             border_width=1, border_style=line.style_dotted)
        f_push_box(tgtBox)
    else
        // Breakout: paint from entry toward measured / points side
        bt = activeDir == 1 ? math.max(longTgtPts, nz(longTgtMm, longTgtPts)) : activePx
        bb = activeDir == 1 ? activePx : math.min(shortTgtPts, nz(shortTgtMm, shortTgtPts))
        tgtBox := box.new(bar_index, bt, bar_index + 1, bb,
             bgcolor=activeDir == 1 ? colTgtLong : colTgtShrt,
             border_color=activeDir == 1 ? colBuy : colSell,
             border_width=1, border_style=line.style_dotted)
        f_push_box(tgtBox)

if locked and firstSigDone and not na(tgtBox)
    box.set_right(tgtBox, bar_index)

if iShowOpp and justSig
    oppY = activeDir == 1 ? longTgtOpp : shortTgtOpp
    lnOpp := line.new(bar_index, oppY, bar_index + 1, oppY,
         color=activeDir == 1 ? colBuy : colSell, width=2)
    f_push_line(lnOpp)
    f_push_lbl(label.new(bar_index, oppY, "OPP END",
         style=activeDir == 1 ? label.style_label_down : label.style_label_up,
         color=color.new(activeDir == 1 ? colBuy : colSell, 0), textcolor=color.white, size=size.tiny))
if not na(lnOpp)
    line.set_x2(lnOpp, bar_index)

if iShowPts and justSig
    ptsY = activeDir == 1 ? longTgtPts : shortTgtPts
    lnPts := line.new(bar_index, ptsY, bar_index + 1, ptsY, color=colSweep, width=1, style=line.style_dashed)
    f_push_line(lnPts)
    f_push_lbl(label.new(bar_index, ptsY, str.tostring(tgtPts, "#.##") + " PTS " + iMkt,
         style=label.style_none, textcolor=colSweep, size=size.tiny))
if not na(lnPts)
    line.set_x2(lnPts, bar_index)

if iShowMM and justSig and not na(rngHgt)
    mmY = activeDir == 1 ? longTgtMm : shortTgtMm
    lnMm := line.new(bar_index, mmY, bar_index + 1, mmY, color=colRev, width=1, style=line.style_dotted)
    f_push_line(lnMm)
if not na(lnMm)
    line.set_x2(lnMm, bar_index)

if firstSigDone and not oppHit and not na(rngHi)
    if firstDir == 1 and high >= rngHi
        oppHit := true
        if iShowSignals
            f_push_lbl(label.new(bar_index, rngHi, "HIGH TGT", style=label.style_label_down, color=colBuy, textcolor=color.white, size=size.tiny))
    if firstDir == -1 and low <= rngLo
        oppHit := true
        if iShowSignals
            f_push_lbl(label.new(bar_index, rngLo, "LOW TGT", style=label.style_label_up, color=colSell, textcolor=color.white, size=size.tiny))

if firstSigDone and not ptsHit and iShowPts
    if firstDir == 1 and high >= firstPx + tgtPts
        ptsHit := true
    if firstDir == -1 and low <= firstPx - tgtPts
        ptsHit := true

if firstSigDone and not eqHit and not na(rngMid)
    if firstDir == 1 and high >= rngMid
        eqHit := true
    if firstDir == -1 and low <= rngMid
        eqHit := true

if firstSigDone and not mmHit and iShowMM and not na(rngHgt)
    if firstDir == 1 and high >= rngHi + rngHgt
        mmHit := true
    if firstDir == -1 and low <= rngLo - rngHgt
        mmHit := true

// ── Status table ──────────────────────────────────────────────────────────
f_pos() =>
    switch iTablePos
        "Top Left" => position.top_left
        "Bottom Right" => position.bottom_right
        "Bottom Left" => position.bottom_left
        => position.top_right

var table tbl = table.new(f_pos(), 2, 12, bgcolor=color.new(#0d0d0d, 12),
     border_color=color.new(#444444, 0), border_width=1, frame_color=color.new(#333333, 0), frame_width=1)

f_cell(c, r, txt, bg, tc) =>
    table.cell(tbl, c, r, txt, text_color=tc, bgcolor=bg, text_size=size.small, text_halign=text.align_left)

if iShowTable and barstate.islast
    phase = not isWeekday ? "Weekend" : inBuild ? "Building " + winLabel : not locked ? "Waiting for window" : firstSide == 0 ? "Locked — wait for break" : not firstSigDone ? "Side taken — wait confirm" : secondSide != 0 and revSigDone ? "Reversal printed" : secondSide != 0 ? "Opposite taken" : oppHit ? "Range target tagged" : "In trade / manage"
    sideTxt = firstSide == 1 ? "HIGH first" + (secondSide == -1 ? " → LOW next" : "") : firstSide == -1 ? "LOW first" + (secondSide == 1 ? " → HIGH next" : "") : "—"
    sigTxt  = revSigDone ? (revDir == 1 ? "REV BUY" : "REV SELL") : firstDir == 1 ? "BUY" : firstDir == -1 ? "SELL" : "None yet"
    rngTxt  = na(rngHi) ? "—" : str.tostring(rngHi, format.mintick) + " / " + str.tostring(rngLo, format.mintick)
    hgtTxt  = na(rngHgt) ? "—" : str.tostring(rngHgt, format.mintick)
    hdrBg   = color.new(#6b1212, 20)
    f_cell(0, 0, iMkt + "  " + winLabel, hdrBg, color.white)
    f_cell(1, 0, "34ematrader", hdrBg, color.new(#ffd54f, 0))
    f_cell(0, 1, "Preset / style", color.new(#1a1a1a, 0), color.gray)
    f_cell(1, 1, iPreset + "  ·  " + iStyle, color.new(#1a1a1a, 0), color.white)
    f_cell(0, 2, "Phase", color.new(#111111, 0), color.gray)
    f_cell(1, 2, phase, color.new(#111111, 0), color.white)
    f_cell(0, 3, "Range Hi / Lo", color.new(#1a1a1a, 0), color.gray)
    f_cell(1, 3, rngTxt, color.new(#1a1a1a, 0), color.white)
    f_cell(0, 4, "Height / pts", color.new(#111111, 0), color.gray)
    f_cell(1, 4, hgtTxt + "   |   " + str.tostring(tgtPts, "#.##") + " " + iMkt, color.new(#111111, 0), color.white)
    f_cell(0, 5, "Breaks", color.new(#1a1a1a, 0), color.gray)
    f_cell(1, 5, sideTxt, color.new(#1a1a1a, 0), firstSide == 1 ? colSell : firstSide == -1 ? colBuy : color.gray)
    f_cell(0, 6, "Signal", color.new(#111111, 0), color.gray)
    f_cell(1, 6, sigTxt, color.new(#111111, 0), revSigDone ? colRev : firstDir == 1 ? colBuy : firstDir == -1 ? colSell : color.gray)
    f_cell(0, 7, "Targets hit", color.new(#1a1a1a, 0), color.gray)
    hitStr = (oppHit ? "END ✓  " : "END ·  ") + (ptsHit ? "PTS ✓  " : "PTS ·  ") + (eqHit ? "EQ ✓" : "EQ ·")
    f_cell(1, 7, hitStr, color.new(#1a1a1a, 0), color.white)
    f_cell(0, 8, "TP / SL", color.new(#111111, 0), color.gray)
    slTxt = iSlMode == "ATR" ? str.tostring(iSlAtr, "#.##") + " ATR" : iSlMode
    f_cell(1, 8, str.tostring(iTpAtr, "#.##") + " ATR  /  " + slTxt, color.new(#111111, 0), color.white)
    wrCol = na(winRate) ? color.gray : winRate >= 50 ? colBuy : colSell
    wrTxt = na(winRate) ? "—" : str.tostring(winRate, "#.#") + "%"
    f_cell(0, 9, "Win rate", color.new(#1a1a1a, 0), color.gray)
    f_cell(1, 9, wrTxt + "   " + str.tostring(statWins) + "W / " + str.tostring(statLoss) + "L / " + str.tostring(statExp) + "exp", color.new(#1a1a1a, 0), wrCol)
    avgR = statClosed > 0 ? (statWinR - statLossR) / statClosed : na
    f_cell(0, 10, "Avg R  (closed)", color.new(#111111, 0), color.gray)
    f_cell(1, 10, na(avgR) ? "—" : str.tostring(avgR, "#.##") + " R", color.new(#111111, 0), color.white)
    posTxt = posDir == 0 ? "Flat" : (posDir == 1 ? "LONG @ " : "SHORT @ ") + str.tostring(posEntry, format.mintick)
    f_cell(0, 11, "Paper trade", color.new(#1a1a1a, 0), color.gray)
    f_cell(1, 11, posTxt, color.new(#1a1a1a, 0), posDir == 1 ? colBuy : posDir == -1 ? colSell : color.gray)

// ── Alerts ────────────────────────────────────────────────────────────────
alertcondition(iAlertSweep and sweepNow and firstSide == 1,  title="Range HIGH taken", message="Range HIGH taken")
alertcondition(iAlertSweep and sweepNow and firstSide == -1, title="Range LOW taken",  message="Range LOW taken")
alertcondition(iAlertSig and buyFirst,  title="BUY",  message="Time-range BUY")
alertcondition(iAlertSig and sellFirst, title="SELL", message="Time-range SELL")
alertcondition(iAlertRev and buyRev,    title="REV BUY",  message="Reversal BUY — opposite side taken")
alertcondition(iAlertRev and sellRev,   title="REV SELL", message="Reversal SELL — opposite side taken")
alertcondition(iAlertTgt and oppHit and not oppHit[1], title="Range end tagged", message="Opposing range end tagged")
alertcondition(tpHitNow, title="ATR TP hit", message="Paper trade take profit hit")
alertcondition(slHitNow, title="SL hit", message="Paper trade stop loss hit")

plot(rngHi, "Range High", color=color.new(colBoxBd, 100), display=display.data_window)
plot(rngLo, "Range Low",  color=color.new(colBoxBd, 100), display=display.data_window)
plot(rngMid, "Range Mid", color=color.new(colMid, 100), display=display.data_window)
plot(firstDir, "First Dir", display=display.data_window)
plot(revDir, "Rev Dir", display=display.data_window)
